package com.mazaiting.mybatisplus.encrypt.plugins;

import com.mazaiting.mybatisplus.encrypt.FieldEncrypt;
import com.mazaiting.mybatisplus.encrypt.IEncryptor;
import com.mazaiting.mybatisplus.config.MybatisCryptoConfig;
import com.mazaiting.mybatisplus.encrypt.exception.MybatisCryptoException;
import com.mazaiting.mybatisplus.encrypt.provider.EncryptedFieldsProvider;
import com.mazaiting.mybatisplus.encrypt.provider.EncryptorProvider;
import com.mazaiting.mybatisplus.encrypt.utils.MybatisPlusUtil;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.util.*;

/**
 * 加密拦截器
 * @author danghailiang
 */
@Intercepts({
        // 插入以及修改进入该过滤器
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MybatisEncryptionPlugin implements Interceptor {
    /**
     * 日志服务
     */
    private final Logger logger = LoggerFactory.getLogger(getClass());
    /**
     * mybatis @Param 注解下需要加解密的参数 key 前缀
     */
    private final List<String> mappedKeyPrefixes;
    /**
     * 快速失败
     */
    private final boolean failFast;
    /**
     * 签名
     */
    private final String key;
    /**
     * 指定默认加密器
     */
    private final Class<? extends IEncryptor> encryptor;

    /**
     * 初始化赋值
     * @param myBatisCryptoConfig 配置类
     */
    public MybatisEncryptionPlugin(MybatisCryptoConfig myBatisCryptoConfig) {
        this.mappedKeyPrefixes = myBatisCryptoConfig.getMappedKeyPrefixes();
        this.failFast = myBatisCryptoConfig.isFailFast();
        this.key = myBatisCryptoConfig.getKey();
        this.encryptor = myBatisCryptoConfig.getEncryptor();
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取参数默认有俩参数
        Object[] args = invocation.getArgs();
        // 获取MappedStatement对象
        MappedStatement ms = (MappedStatement) args[0];
        // 获取SqlCommandType
        SqlCommandType sqlCommandType = ms.getSqlCommandType();
        // 获取需要加密的对象
        Object parameter = args[1];
        // 符合加密器条件
        if (MybatisPlusUtil.encryptionRequired(parameter, sqlCommandType)) {
            if (parameter instanceof MapperMethod.ParamMap) {
                //noinspection unchecked
                MapperMethod.ParamMap<Object> paramMap = (MapperMethod.ParamMap<Object>) parameter;
                encryptParamMap(paramMap);
            } else {
                encryptEntity(parameter);
            }
        }
        return invocation.proceed();
    }

    /**
     * 传入需要被加密的对象
     * @param parameter 待加密对象
     * @throws MybatisCryptoException 捕获异常
     */
    private void encryptEntity(Object parameter) throws MybatisCryptoException {
        processFields(EncryptedFieldsProvider.get(parameter.getClass()), parameter);
    }

    /**
     * map加密
     * @param paramMap 待加密集合
     * @throws MybatisCryptoException 自定义异常捕获
     */
    private void encryptParamMap(MapperMethod.ParamMap<Object> paramMap) throws MybatisCryptoException {
        // 获取map的Entry的set集合
        Set<Map.Entry<String, Object>> entrySet = paramMap.entrySet();
        // 遍历map集合
        for (Map.Entry<String, Object> entry : entrySet) {
            // 获取键
            String key = entry.getKey();
            // 获取值
            Object value = entry.getValue();
            // 非空判断
            if (Objects.isNull(value) || !StringUtils.hasLength(key)) {
                continue;
            }
            // 遍历准备好的 前缀
            for (String mappedKeyPrefix : mappedKeyPrefixes) {
                // 判断ket是否包含此前缀
                if (key.startsWith(mappedKeyPrefix)) {
                    // 判断值是否为ArrayList类型或其子类
                    if (value instanceof ArrayList list) {
                        // 非空判断
                        if (!list.isEmpty()) {
                            // 获取值
                            Object firstItem = list.get(0);
                            // 获取字节码对象
                            Class<?> itemClass = firstItem.getClass();
                            // 获取对应属性
                            Set<Field> encryptedFields = EncryptedFieldsProvider.get(itemClass);
                            // 遍历
                            for (Object item : list) {
                                processFields(encryptedFields, item);
                            }
                        }
                    } else {
                        processFields(EncryptedFieldsProvider.get(value.getClass()), value);
                    }
                }
            }
        }
    }

    /**
     * 加密属性
     * @param encryptedFields 属性
     * @param entry 具体的值
     * @throws MybatisCryptoException 捕获异常
     */
    private void processFields(Set<Field> encryptedFields, Object entry) throws MybatisCryptoException {
        // 非空判断
        if (Objects.isNull(encryptedFields) || encryptedFields.isEmpty()) {
            return;
        }
        // 遍历
        for (Field field : encryptedFields) {
            // 获取属性值上的注解对象
            FieldEncrypt fieldEncrypt = field.getAnnotation(FieldEncrypt.class);
            // 为空则结束
            if (Objects.isNull(fieldEncrypt)) {
                continue;
            }
            try {
                // 获取签名
                String key = MybatisPlusUtil.getKey(fieldEncrypt, this.key);
                // 获取处理器
                IEncryptor iEncryptor = EncryptorProvider.get(fieldEncrypt, encryptor);
                // 暴力破解
                field.setAccessible(true);
                // 获取属性值
                Object originalVal = field.get(entry);
                if (Objects.isNull(originalVal)) {
                    continue;
                }
                // 加密后的值
                String encryptedVal = iEncryptor.encrypt(originalVal, key);
                // 设置新值
                field.set(entry, encryptedVal);
            } catch (Exception e) {
                if (failFast) {
                    throw new MybatisCryptoException(e);
                } else {
                    logger.warn("process encrypted filed error.", e);
                }
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
    }
}
